/*
CloudLink Phi Extension for Scratch 3.0

MIT License

Copyright (C) 2024 Mike Renaker "MikeDEV".

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/

// CloudLink Phi
// ID: cloudlinkphi
// Description: Simply, Quickly, Easily.
// By: MikeDEV
// License: MIT
(async function (Scratch2) {
    'use strict';

    // Error handling for fatal issues
	function fatalAlert(message) {
		alert(message);
		throw new Error(message);
	}

    // Require the extension to be unsandboxed
    if (!Scratch2.extensions.unsandboxed) {
        fatalAlert("CloudLink Phi 扩展无法在沙盒模式下运行。");
    }

    // Require access to the VM and/or runtime
    if (!Scratch2.vm || !Scratch2.vm.runtime) {
        fatalAlert("CloudLink Phi 扩展无法检测到对 Scratch VM 或运行时的访问。此 Scratch 环境可能不受支持。");
    }

    // Require the browser to support WebRTC (used for connectivity)
    if (!RTCPeerConnection) {
        fatalAlert("CloudLink Phi 扩展无法检测到 WebRTC 支持。");
    }

    // Require browser to support WebSockets (used for signaling)
    if (!WebSocket) {
        fatalAlert("CloudLink Phi 扩展无法检测到 WebSocket 支持。");
    }

    // Require browser to support Web Locks API (used for concurrency)
    if (!navigator.locks) {
        fatalAlert("CloudLink Phi 扩展无法检测到 Web Locks 支持。");
    }

    // https://github.com/skeeto/ulid-js - Thank you, skeeto
    function ULID() {
        const BASE32 = [
            '0', '1', '2', '3', '4', '5', '6', '7',
            '8', '9', 'A', 'B', 'C', 'D', 'E', 'F',
            'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q',
            'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'
        ];
        let last = -1;
        /* Pre-allocate work buffers / views */
        let ulid = new Uint8Array(16);
        let time = new DataView(ulid.buffer, 0, 6);
        let rand = new Uint8Array(ulid.buffer, 6, 10);
        let dest = new Array(26);
    
        function encode(ulid) {
            dest[0] = BASE32[ ulid[0] >> 5];
            dest[1] = BASE32[(ulid[0] >> 0) & 0x1f];
            for (let i = 0; i < 3; i++) {
                dest[i*8+2] = BASE32[ ulid[i*5+1] >> 3];
                dest[i*8+3] = BASE32[(ulid[i*5+1] << 2 | ulid[i*5+2] >> 6) & 0x1f];
                dest[i*8+4] = BASE32[(ulid[i*5+2] >> 1) & 0x1f];
                dest[i*8+5] = BASE32[(ulid[i*5+2] << 4 | ulid[i*5+3] >> 4) & 0x1f];
                dest[i*8+6] = BASE32[(ulid[i*5+3] << 1 | ulid[i*5+4] >> 7) & 0x1f];
                dest[i*8+7] = BASE32[(ulid[i*5+4] >> 2) & 0x1f];
                dest[i*8+8] = BASE32[(ulid[i*5+4] << 3 | ulid[i*5+5] >> 5) & 0x1f];
                dest[i*8+9] = BASE32[(ulid[i*5+5] >> 0) & 0x1f];
            }
            return dest.join('');
        }
    
        return function() {
            let now = Date.now();
            if (now === last) {
                /* 80-bit overflow is so incredibly unlikely that it's not
                    * considered as a possiblity here.
                    */
                for (let i = 9; i >= 0; i--)
                    if (rand[i]++ < 255)
                        break;
            } else {
                last = now;
                time.setUint16(0, (now / 4294967296.0) | 0);
                time.setUint32(2, now | 0);
                window.crypto.getRandomValues(rand);
            }
            return encode(ulid);
        };
    }

    class Encryption {
        async generateKeyPair() {
            let keyPair = await window.crypto.subtle.generateKey(
                {
                    name: "ECDH",
                    namedCurve: "P-256"
                },
                true,
                ["deriveKey", "deriveBits"]
            );
            let publicKey = await this.exportPublicKey(keyPair.publicKey);
            let privateKey = await this.exportPrivateKey(keyPair.privateKey);
            return [publicKey, privateKey];
        }

        async exportPublicKey(pubKey) {
            let exportedKey = await window.crypto.subtle.exportKey("spki", pubKey);
            return this.arrayBufferToBase64(new Uint8Array(exportedKey));
        }

        async importPublicKey(exportedKey) {
            const exportedKeyArray = this.base64ToArrayBuffer(exportedKey);
            return await window.crypto.subtle.importKey(
                "spki",
                exportedKeyArray,
                {
                    name: "ECDH",
                    namedCurve: "P-256"
                },
                true,
                []
            );
        }

        async exportPrivateKey(privKey) {
            let exportedKey = await window.crypto.subtle.exportKey("pkcs8", privKey);
            return this.arrayBufferToBase64(new Uint8Array(exportedKey));
        }

        async importPrivateKey(exportedKey) {
            const exportedKeyArray = this.base64ToArrayBuffer(exportedKey);
            return await window.crypto.subtle.importKey(
                "pkcs8",
                exportedKeyArray,
                {
                    name: "ECDH",
                    namedCurve: "P-256"
                },
                true,
                ["deriveKey", "deriveBits"]
            );
        }

        async deriveSharedKey(publicKey, privateKey) {
            let pubkey = await this.importPublicKey(publicKey);
            let privkey = await this.importPrivateKey(privateKey);
            let shared = await window.crypto.subtle.deriveKey(
                {
                    name: "ECDH",
                    public: pubkey
                },
                privkey,
                {
                    name: "AES-GCM",
                    length: 256
                },
                true,
                ["encrypt", "decrypt"]
            );
            let exported = await this.exportSharedKey(shared);
            return exported;
        }

        async exportSharedKey(sharedKey) {
            let exportedKey = await window.crypto.subtle.exportKey("raw", sharedKey);
            return this.arrayBufferToBase64(new Uint8Array(exportedKey));
        }

        async importSharedKey(exportedKey) {
            const exportedKeyArray = this.base64ToArrayBuffer(exportedKey);
            return await window.crypto.subtle.importKey(
                "raw",
                exportedKeyArray,
                {
                    name: "AES-GCM",
                    length: 256
                },
                true,
                ["encrypt", "decrypt"]
            );
        }

        async encrypt(message, sharedKey) {
            let shared = await this.importSharedKey(sharedKey);
            let encodedMessage = new TextEncoder().encode(message);
            const iv = window.crypto.getRandomValues(new Uint8Array(12));
            const encryptedMessage = await window.crypto.subtle.encrypt(
                {
                    name: "AES-GCM",
                    iv: iv
                },
                shared,
                encodedMessage
            );
            const encryptedMessageArray = new Uint8Array(encryptedMessage);
            const encryptedMessageBase64 = this.arrayBufferToBase64(encryptedMessageArray);
            const ivBase64 = this.arrayBufferToBase64(iv);
            return [encryptedMessageBase64, ivBase64];
        }

        async decrypt(encryptedMessageBase64, ivBase64, sharedKey) {
            let shared = await this.importSharedKey(sharedKey);
            let encryptedMessageArray = this.base64ToArrayBuffer(encryptedMessageBase64);
            const iv = this.base64ToArrayBuffer(ivBase64);
            const decryptedMessage = await window.crypto.subtle.decrypt(
                {
                    name: "AES-GCM",
                    iv: iv
                },
                shared,
                encryptedMessageArray
            );
            const decodedMessage = new TextDecoder().decode(decryptedMessage);
            return decodedMessage;
        }

        arrayBufferToBase64(buffer) {
            let binary = '';
            let bytes = new Uint8Array(buffer);
            for (let i = 0; i < bytes.byteLength; i++) {
                binary += String.fromCharCode(bytes[i]);
            }
            return btoa(binary);
        }

        base64ToArrayBuffer(base64) {
            let binary_string = window.atob(base64);
            let len = binary_string.length;
            let bytes = new Uint8Array(len);
            for (let i = 0; i < len; i++) {
                bytes[i] = binary_string.charCodeAt(i);
            }
            return bytes.buffer;
        }
    }

    class PhiClient {
        constructor() {

            // Config
            this.stun_url = "stun:vpn.mikedev101.cc:5349";
            this.turn_url = "turn:vpn.mikedev101.cc:5349";
            this.turn_username = "free";
            this.turn_password = "free";
            this.turn_only = false;

            // Metadata
            this.metadata = {
                client_type: "phi",
                client_version: "1.0.0",
                protocol_version: "1",
                signaling_version: "1.2",
                user_agent: window.navigator.userAgent,
                encryption_suite: "ECDH-P256-AES-GCM",
                key_exchange_mode: "SPKI-BASE64",
            };

            // State
            this.peers = new Map();
            this.messageCallbacks = new Map();
            this.eventCallbacks = new Map();
            this.peerConnectedEvents = new Map();
            this.peerDisconnectedEvents = new Map();
            this.privateMessageCallbacks = new Map();
            this.seenPeers = new Map();
            this.id = null;
            this.session = null;
            this.username = null;
            this.peerNewEvent = null;
            this.peerSupportsEncryptionEvent = null;
            this.peerMadeOwnerEvent = null;
            this.connectedEvent = null;
            this.modeChangeEvent = null;
            this.disconnectedEvent = null;
            this.usernameSetEvent = null;
            this.fetchedLobbyListEvent = null;
            this.fetchedLobbyInfoEvent = null;
            this.keepalive = null;
            this.usernameSet = false;

            /* Broadcast storage 
            {
                data: null,
                origin: null,
            }; */
            this.broadcastEvent = null;
            this.broadcastStore = new Map();

            // Flags
            this.relayEnabled = false;
            this.enableKeepalive = true;
            this.mode = 0; // 0 - none, 1 - host, 2 - peer

            // End-to-end Encryption Support
            this.encryption = new Encryption();
            this.publicKey = "";
            this.privateKey = "";

            // LAN Support
            this.net;
            this.dgram;
            this.lan_feature_supported;
            this.in_lan_mode = false;
            this.hosting_lan = false;

            this.is_lan_connected = false;
            this.is_lan_searching = false;

            this.discovery_client;
            this.discovery_server;
            this.discovery_interval;

            this.lan_client;
            this.lan_host;

            this.lan_hosts = [];
            this.lan_clients = [];

            // Check if LAN feature is supported
            try {
                this.net = require('net');
                this.dgram = require('dgram');
                this.lan_feature_supported = true;
                console.log("看起来该环境可以访问 NodeJS API。局域网功能已启用。");
            } catch (e) {
                this.lan_feature_supported = false;
                console.warn("看起来该环境无法访问 NodeJS API。局域网功能已禁用。错误：" + e);
            }
        }

        ConnectWithLAN(host, username) {
            const self = this;
            if (!self.lan_feature_supported) {
                console.warn("LAN feature is not supported, can't connect to a LAN host.");
                return;
            }
            
            // Can't be connected to a server
            if (self.Connected()) return;

            // Can't be in LAN mode already
            if (self.in_lan_mode) return;
            self.in_lan_mode = true;

            // Generate a ULID for this client
            const generator = new ULID();
            self.id = generator();
            self.username = username;

            // Connect to server
            self.lan_client = self.net.createConnection({ host, port: 3100 }, () => {
                console.log(`已连接到主机 ${host}！`);
                self.is_lan_connected = true;

                // Tell the server to DISCOVER us
                self.lan_client.write(JSON.stringify({
                    opcode: "DISCOVER",
                    payload: {
                        id: self.id,
                        user: self.username,
                        pubkey: self.publicKey,
                    }
                }));

                if (self.enableKeepalive) {
                    self.emitMessage("KEEPALIVE", null, null, true, self.lan_client);
                }
            });

            // Create metadata storage since we're in LAN mode (for the "origin" and "recipient" fields in the CL5 protocol)
            self.lan_client.metadata = {};

            // Listen for data
            self.lan_client.on('data', async (data) => {
                await self.handleLANMessages(data, self.lan_client, false);
            });

            self.lan_client.on('error', (error) => {
                console.error("局域网客户端错误：", error);
                self.lan_client.end();
            });

            // Handle connection close
            self.lan_client.on('close', () => {
                console.log('已从局域网主机断开连接。');
                self.in_lan_mode = false;
                self.lan_client = null;
                self.is_lan_connected = false;
            });
        }

        FindLANHosts() {
            const self = this;
            if (!self.lan_feature_supported) {
                console.warn("LAN feature is not supported, can't discover hosts.");
                return;
            }
            
            // Can't be connected to a server
            if (self.Connected()) return;

            // Can't be hosting a LAN server
            if (self.hosting_lan) return;

            // Can't be scanning already
            if (self.is_lan_searching) return;
            self.is_lan_searching = true;
            
            // discovery_client.js
            self.lan_hosts = [];
            self.discovery_client = self.dgram.createSocket('udp4');

            // Listen for any broadcast messages
            self.discovery_client.on('message', (message, rinfo) => {
                if (message != "Packets in the stream, Voices ripple, data gleams- Signals cross the world.") return;
                console.log(`在 ${rinfo.address} 发现局域网主机`);
                if (!self.lan_hosts.includes(rinfo.address)) self.lan_hosts.push(rinfo.address);
            });

            // Show events
            self.discovery_client.on('listening', () => {
                console.log('正在查找局域网主机...');

                // Wait for up to 30 seconds to find hosts, then shut down the discovery client
                setTimeout(() => {
                    self.discovery_client.close();
                }, 30000);

            })

            self.discovery_client.on('close', () => {
                console.log('发现客户端已关闭。');
                self.is_lan_searching = false;
            })

            self.discovery_client.on('error', (err) => {
                console.error(`发现客户端错误：${err}`);
                self.discovery_client.close();
            })

            // Begin listening for hosts to find
            self.discovery_client.bind(3101);
        }

        RunLANServer(username) {

            // TODO: clean up this code

            const self = this;
            if (!self.lan_feature_supported) {
                console.warn("LAN feature is not supported, can't run server.");
                return;
            }

            // Can't be connected to a server
            if (self.Connected()) return;

            // Can't already be hosting
            if (self.hosting_lan) return;
            self.hosting_lan = true;
            
            // Can't be in LAN mode already
            if (self.in_lan_mode) return;
            self.in_lan_mode = true;

            // Generate a ULID for this client
            const generator = new ULID();
            self.id = generator();
            self.username = username;

            // server.js
            self.lan_clients = [];
            self.lan_host = self.net.createServer((socket) => {
                self.lan_clients.push(socket);
                console.log(`来自 ${socket.remoteAddress}:${socket.remotePort} 的新局域网连接`);
                socket.metadata = {};

                // Tell the peer to ANTICIPATE us
                socket.write(JSON.stringify({
                    opcode: "ANTICIPATE",
                    payload: {
                        id: self.id,
                        user: self.username,
                        pubkey: self.publicKey,
                    }
                }));

                socket.on('data', async (data) => {
                    await self.handleLANMessages(data, socket, true);
                });

                socket.on('close', () => {
                    console.log(`来自 ${socket.remoteAddress}:${socket.remotePort} 的局域网连接已关闭`);
                    self.lan_clients.splice(self.lan_clients.indexOf(socket), 1);
                });
            });

            self.lan_host.on('listening', () => {
                console.log(`局域网主机正在运行！`);
            })

            // discovery_server.js
            self.discovery_server = self.dgram.createSocket('udp4');

            // Send multicast announcement
            self.discovery_interval = setInterval(() => {

                // Send some magic message for discovery. This haiku will be multicasted at every second.
                const message = Buffer.from("Packets in the stream, Voices ripple, data gleams- Signals cross the world.");
                self.discovery_server.send(message, 0, message.length, 3101, '255.255.255.255');

            }, 1000);

            self.discovery_server.on('listening', () => {
                console.log('在 LAN UDP 端口 3101 上广播发现消息...');
            })

            self.discovery_server.on('close', () => {
                console.log('发现服务器已关闭。');
                self.discovery_server = null;
                clearInterval(self.discovery_interval);
            })

            self.discovery_server.on('error', (err) => {
                console.error(`Discovery error: ${err}`);
                self.discovery_server.close();
                self.lan_host.close();
            })

            // Begin broadcasting discovery messages for players to find
            self.discovery_server.bind(3101, () => {
                self.discovery_server.setBroadcast(true);
                self.discovery_server.setMulticastTTL(128);
            })

            self.lan_host.on('close', () => {
                console.log('局域网主机已关闭。');
                self.hosting_lan = false;
                self.in_lan_mode = false;
                self.lan_host = null;
                self.Close();
            })

            self.lan_host.on('error', (err) => {
                console.error(`主机错误：${err}`);
                self.discovery_server.close();
                self.lan_host.close();
            })

            // Begin running LAN server
            self.lan_host.listen(3100);
        }

        StopLANServer() {
            const self = this;
            if (!self.lan_feature_supported) return;
            if (!self.hosting_lan) return;

            // Tell the LAN server that we're shutting things down
            self.lan_host.close();
            self.discovery_server.close();

            // Finalize shutdown by closing connections, which is required
            self.lan_clients.forEach((client) => {
                client.end();
            });

            // Finally, close all WebRTC connections
            self.Close();
        }

        DisconnectFromLAN() {
            const self = this;
            if (!self.lan_feature_supported) return;
            if (!self.lan_client) return;

            // Close the connection to the host
            self.lan_client.end();

            // Finally, close all WebRTC connections
            self.Close();
        }

        Connected() {
            const self = this;
            return (self.socket != null && self.socket.readyState == WebSocket.OPEN);
        }

        Connect(url) {
            const self = this;

            // Can't connect if already connected
            if (self.socket) return;

            // Can't be in LAN mode
            if (self.in_lan_mode) return;

            self.socket = new WebSocket(url);

            self.socket.onopen = async () => {
                // Generate public and private keys for end-to-end encryption
                self.emitMessage("META", self.metadata, null);
                if (self.connectedEvent) self.connectedEvent();

                if (self.enableKeepalive) {
                    self.emitMessage("KEEPALIVE", null, null);
                }
            };

            self.socket.onmessage = async (event) => await self.handleSignalingMessage(JSON.parse(event.data), false, null);

            self.socket.onerror = (error) => console.error("WebSocket error:", error);

            self.socket.onclose = async () => {
                self.Close();
                if (self.disconnectedEvent) self.disconnectedEvent();
            }
        }

        SetUsername(username) {
            const self = this;
            if (!self.socket || self.socket.readyState != WebSocket.OPEN) return;
            if (self.username) return;
            self.username = username;
            self.emitMessage("INIT", username, null);
        }

        FetchRoomList() {
            const self = this;
            self.emitMessage("LOBBY_LIST", null, null)
        }

        OnGlobalBroadcast(callback) {
            const self = this;
            self.broadcastEvent = callback;
        }

        OnPeerPrivateMessage(peer, callback) {
            const self = this;
            self.privateMessageCallbacks.set(peer, callback);
        }

        OnLobbyList(callback) {
            const self = this;
            self.fetchedLobbyListEvent = callback;
        }

        OnLobbyInfo(callback) {
            const self = this;
            self.fetchedLobbyInfoEvent = callback;
        }

        WhenConnected(callback) {
            const self = this;
            self.connectedEvent = callback;
        }

        WhenDisconnected(callback) {
            const self = this;
            self.disconnectedEvent = callback;
        }

        OnNewPeer(callback) {
            const self = this;
            self.peerNewEvent = callback;
        }

        WhenUsernameSet(callback) {
            const self = this;
            self.usernameSetEvent = callback;
        }

        WhenPeerSupportsEncryption(callback) {
            const self = this;
            self.peerSupportsEncryptionEvent = callback;
        }

        WhenChangingMode(callback) {
            const self = this;
            self.modeChangeEvent = callback;
        }

        OnOwnershipChange(callback) {
            const self = this;
            self.peerMadeOwnerEvent = callback;
        }

        OnPeerMessage(peer, callback) {
            const self = this;
            self.messageCallbacks.set(peer, callback);
        }

        OnPeerConnected(peer, callback) {
            const self = this;
            self.peerConnectedEvents.set(peer, callback);
        }

        OnPeerDisconnected(peer, callback) {
            const self = this;
            self.peerDisconnectedEvents.set(peer, callback);
        }

        BindEvent(opcode, callback) {
            const self = this;
            self.eventCallbacks.set(opcode, callback);
        }

        FireSeenPeerEvent(peer, opcode) {
            const self = this;
            if (!self.seenPeers.has(peer.id)) return;
            if (!self.peerNewEvent) return;
            if (self.seenPeers.get(peer.id)) return;
            if (peer.id === "relay" && !self.relayEnabled) self.relayEnabled = true;
            self.seenPeers.set(peer.id, true);
            self.peerNewEvent(peer, opcode);
        }

        async createConnection(peerId, username, pubKey = null, is_lan = false, lan_socket = null) {
            const self = this;

            if (self.peers.has(peerId)) {
                throw new Error(`Connection with peer ${username} (${peerId}) already exists`);
            }
            if (!peerId) {
                throw new Error(`Got a null peerId`);
            }
            if (!username) {
                throw new Error(`Got a null username`);
            }

            let sharedKey = null;
            if (pubKey) {
                sharedKey = await self.encryption.deriveSharedKey(pubKey, self.privateKey);
                if (self.peerSupportsEncryptionEvent) self.peerSupportsEncryptionEvent({user: username, id: peerId});
            }

            const peerConnection = {
                conn: new RTCPeerConnection({
                    iceServers: [
                        { urls: this.stun_url },
                        { urls: this.turn_url, username: this.turn_username, credential: this.turn_password },
                    ],
                    iceTransportPolicy: (this.turn_only ? "relay" : "all"),
                }),
                username,
                sharedKey,
                dataChannels: new Map(),
                destroyedChannels: new Array(),
                /* Private Channels: {
                    data: null,
                },*/ 
                privateStore: new Map(),
            }

            const dataChannel = peerConnection.conn.createDataChannel("default", { protocol: "clomega", negotiated: true, id: 0, ordered: true });
            peerConnection.dataChannels.set("default", dataChannel);
            self.bindChannelHandlers(dataChannel, peerId);

            peerConnection.conn.ondatachannel = async (event) => {
                if (!event.channel) return;
                await navigator.locks.request(peerId + "_channels", () => {
                    // Even with an acquired lock, there is always the possibility of the other peer opening the same channel at the same time.
                    // If this happens, our existing channel will be overwritten by the new one.
                    const dataChannel = event.channel;
                    peerConnection.dataChannels.set(dataChannel.label, dataChannel);
                    self.bindChannelHandlers(dataChannel, peerId);
                    console.log(`${username} (${peerId}) 已打开通道 ${dataChannel.label}。`);
                });
            }

            peerConnection.conn.onconnectionstatechange = async () => {
                switch (peerConnection.conn.connectionState) {
                    case "closed":
                    case "disconnected":
                        console.log(`已与对等方断开连接 ${peerId} (${username})`);
                        break;
                    case "connected":
                        console.log(`已连接到对等方 ${peerId} (${username})`);
                        break;
                    case "failed":
                        console.log(`连接对等方失败 ${peerId} (${username})`);
                        await self.closeConnection(peerId);
                        break;
                }
            }

            peerConnection.conn.onicecandidate = async(event) => {
                if (event.candidate) {
                    if (peerConnection.sharedKey) {
                        let [encrypted, iv] = await self.encryption.encrypt(JSON.stringify(event.candidate), peerConnection.sharedKey);
                        self.emitMessage("ICE", { type: 0, contents: [encrypted, iv]}, peerId, is_lan, lan_socket);
                    } else {
                        self.emitMessage("ICE", { type: 0, contents: event.candidate }, peerId, is_lan, lan_socket);
                    }
                }
            }

            self.peers.set(peerId, peerConnection);
            self.seenPeers.set(peerId, false);
            return peerConnection;
        }

        async OpenChannel(peerId, channel, ordered) {
            const self = this;
            return navigator.locks.request(peerId + "_channels", () => {
                if (!self.peers.has(peerId)) return;
                const peerConnection = self.peers.get(peerId);
                if (peerConnection.dataChannels.has(channel)) return;
                const dataChannel = peerConnection.conn.createDataChannel(channel, { protocol: "clomega", ordered });
                peerConnection.dataChannels.set(channel, dataChannel);
                self.bindChannelHandlers(dataChannel, peerId);
            });
        }

        async CloseChannel(peerId, channel) {
            const self = this;
            return navigator.locks.request(peerId + "_channels", () => {
                if (!self.peers.has(peerId)) return;
                const peerConnection = self.peers.get(peerId);
                if (!peerConnection.dataChannels.has(channel)) return;
                if (channel == "default") {
                    console.warn("Cannot close default data channel. Close the connection with the peer instead.");
                    return;
                };
                const dataChannels = peerConnection.dataChannels;
                if (!dataChannels.has(channel)) return;
                dataChannels.get(channel).close();
            });
        }

        bindChannelHandlers(channel, peerId) {
            const self = this;

            channel.onopen = () => {
                const peerConnection = self.peers.get(peerId);
                if (peerConnection.destroyedChannels.includes(channel.label)) {
                    peerConnection.destroyedChannels.splice(peerConnection.destroyedChannels.indexOf(channel.label), 1);
                };

                if (peerId == "relay") return;

                if (self.peerConnectedEvents.has(peerId))
                    self.peerConnectedEvents.get(peerId)(channel.label);
            }

            channel.onclose = () =>  {
                if (self.peers.has(peerId)) {
                    const peerConnection = self.peers.get(peerId);
                    peerConnection.dataChannels.delete(channel.label);
                    peerConnection.privateStore.delete(channel.label);
                    if (!peerConnection.destroyedChannels.includes(channel.label)) peerConnection.destroyedChannels.push(channel.label);
                }
                if ((channel.label != "default") && (self.peerDisconnectedEvents.has(peerId)))
                    self.peerDisconnectedEvents.get(peerId)(channel.label);
            }

            channel.onmessage = async (event) => {
                let packet = JSON.parse(event.data);

                // Read data and handle accordingly if relayed
                let relayed = false;
                let originPeer = peerId;
                let intendedChannel = channel;

                if (packet.origin) {
                    originPeer = packet.origin.id;
                    relayed = true;
                }

                if (!self.peers.has(originPeer)) {
                    console.warn(`Peer ${originPeer} not found.`);
                    return;
                }
                
                if (packet.channel) {
                    
                    // Check if channel exists
                    if (!self.peers.get(originPeer).dataChannels.has(packet.channel)) {
                        console.warn(`Channel ${packet.channel} not found with peer ${originPeer}.`);
                        return;
                    }

                    intendedChannel = self.peers.get(originPeer).dataChannels.get(packet.channel);
                }

                // Decrypt if encrypted
                let pc = self.peers.get(originPeer);
                if (pc.sharedKey) { 
                    packet.payload = await this.encryption.decrypt(packet.payload[0], packet.payload[1], pc.sharedKey);
                }

                // Handle opcode-specific callbacks
                const peerConnection = self.peers.get(originPeer);
                switch (packet.opcode) {
                    
                    case "G_MSG":
                        self.broadcastStore.set(intendedChannel.label, {
                            data: packet.payload,
                            origin: originPeer,
                        });
                        if (self.broadcastEvent) self.broadcastEvent(intendedChannel.label, packet.payload, originPeer);
                        return;
                    
                    case "P_MSG":
                        peerConnection.privateStore.set(intendedChannel.label, packet.payload);
                        if (self.privateMessageCallbacks.has(originPeer)) self.privateMessageCallbacks.get(originPeer)(intendedChannel.label, packet.payload, relayed);
                        return;

                    // TODO: Handle other in-band opcodes here in Omega client
                }

                // Handle generic callbacks
                if (self.messageCallbacks.has(originPeer)) self.messageCallbacks.get(originPeer)(intendedChannel.label, packet.payload, relayed);
            }
        }

        // Create an offer and send it to the signaling server
        async createOffer(peerId, is_lan = false, lan_socket = null) {
            const self = this;
            if (!self.peers.has(peerId)) {
                throw new Error(`No peer connection found for ${peerId}`);
            }
            const peerConnection = self.peers.get(peerId);
            const offer = await peerConnection.conn.createOffer();
            await peerConnection.conn.setLocalDescription(offer);
            if (peerConnection.sharedKey) {
                let [encrypted, iv] = await self.encryption.encrypt(JSON.stringify(offer), peerConnection.sharedKey);
                self.emitMessage("MAKE_OFFER", { type: 0, contents: [encrypted, iv]}, peerId, is_lan, lan_socket);
            } else {
                self.emitMessage("MAKE_OFFER", { type: 0, contents: offer }, peerId, is_lan, lan_socket);
            }
        }

        // Create an answer in response to an offer
        async createAnswer(peerId, offer, is_lan = false, lan_socket = null) {
            const self = this;
            if (!self.peers.has(peerId)) {
                throw new Error(`No peer connection found for ${peerId}`);
            }
            const peerConnection = self.peers.get(peerId);
            if (peerConnection.sharedKey) { 
                offer = JSON.parse(await self.encryption.decrypt(offer[0], offer[1], peerConnection.sharedKey));
            }
            await peerConnection.conn.setRemoteDescription(new RTCSessionDescription(offer));
            const answer = await peerConnection.conn.createAnswer();
            await peerConnection.conn.setLocalDescription(answer);
            if (peerConnection.sharedKey) {
                let [encrypted, iv] = await self.encryption.encrypt(JSON.stringify(answer), peerConnection.sharedKey);
                self.emitMessage("MAKE_ANSWER", { type: 0, contents: [encrypted, iv]}, peerId, is_lan, lan_socket);
            } else {
                self.emitMessage("MAKE_ANSWER", { type: 0, contents: answer }, peerId, is_lan, lan_socket);
            }
        }

        async handleOffer(peerId, offer, is_lan = false, lan_socket = null) {
            const self = this;
            if (!self.peers.has(peerId)) {
                throw new Error(`No connection found for peer ${peerId}`);
            }
            const peerConnection = self.peers.get(peerId);
            if (peerConnection.sharedKey) { 
                offer = JSON.parse(await self.encryption.decrypt(offer[0], offer[1], peerConnection.sharedKey));
            }
            await peerConnection.conn.setRemoteDescription(new RTCSessionDescription(offer));
            const answer = await peerConnection.conn.createAnswer();
            await peerConnection.conn.setLocalDescription(answer);
            if (peerConnection.sharedKey) {
                let [encrypted, iv] = await self.encryption.encrypt(JSON.stringify(answer), peerConnection.sharedKey);
                self.emitMessage("MAKE_ANSWER", { type: 0, contents: [encrypted, iv]}, peerId,  is_lan, lan_socket);
            } else {
                self.emitMessage("MAKE_ANSWER", { type: 0, contents: answer }, peerId,  is_lan, lan_socket);
            }
        }

        async handleAnswer(peerId, answer) {
            const self = this;
            if (!self.peers.has(peerId)) {
                throw new Error(`No connection found for peer ${peerId}`);
            }
            const peerConnection = self.peers.get(peerId);
            if (peerConnection.sharedKey) { 
                answer = JSON.parse(await self.encryption.decrypt(answer[0], answer[1], peerConnection.sharedKey));
            }
            await peerConnection.conn.setRemoteDescription(new RTCSessionDescription(answer));
        }

        async handleICECandidate(peerId, candidate) {
            const self = this;
            if (!self.peers.has(peerId)) {
                throw new Error(`No connection found for peer ${peerId}`);
            }
            const peerConnection = self.peers.get(peerId);
            if (peerConnection.sharedKey) { 
                candidate = JSON.parse(await self.encryption.decrypt(candidate[0], candidate[1], peerConnection.sharedKey));
            }
            await peerConnection.conn.addIceCandidate(new RTCIceCandidate(candidate));
        }

        async sendMessage(peerId, channel, opcode, data, relay = false, wait = false) {
            const self = this;

            return new Promise((resolve, reject) => {
                if (!self.peers.has(peerId)) {
                    reject(`No connection found for peer ${peerId}`);
                    return;
                }
                if (!["object", "string", "number"].includes(typeof data) && !Array.isArray(data)) {
                    reject("Payload must be an Object, Array, Number, or String.");
                    return;
                }
                let packet = { opcode };
                if (relay) {
                    if (!self.relayEnabled) {
                        reject(`Can't use server-side relay since no relay peer was detected`);
                        return;
                    }
                    packet.recipient = peerId;
                    packet.channel = channel;
                    peerId = "relay";
                    channel = "default";
                }
                const peerConnection = self.peers.get(peerId);
                if (!peerConnection.dataChannels.has(channel)) {
                    reject(`Data channel ${channel} not found with peer ${peerConnection.username} (${peerId})`);
                    return;
                }
                const dataChannel = peerConnection.dataChannels.get(channel);
                if (dataChannel.readyState !== "open") {
                    reject(`Data channel ${channel} with ${peerConnection.username} (${peerId}) is not open.`);
                    return;
                }
                if (peerConnection.sharedKey) {
                    self.encryption.encrypt(JSON.stringify(packet), peerConnection.sharedKey).then(([encrypted, iv]) => {
                        packet.payload = [encrypted, iv];
                        dataChannel.send(JSON.stringify(packet));
                    }).catch((error) => {
                        console.error(`Failed to encrypt payload for peer ${peerId}:`, error);
                    });
                } else {
                    packet.payload = data;
                    dataChannel.send(JSON.stringify(packet));
                }
                if (wait) {
                    const interval = setInterval(() => {
                        if (dataChannel.bufferedAmount === 0) {
                            clearInterval(interval);
                            resolve();
                        }
                    }, 10);
                }
                else {
                    resolve();
                }
            });
        }

        async closeConnection(peerId) {
            const self = this;
            return navigator.locks.request(peerId, () => {
                if (self.peers.has(peerId)) {
                    const peerConnection = self.peers.get(peerId);
                    console.log(`正在关闭与对等方 ${peerConnection.username} (${peerId}) 的连接`);
                    peerConnection.dataChannels.forEach((dataChannel, channelId) => {
                        dataChannel.close();
                    });
                    peerConnection.conn.close();
                    self.peers.delete(peerId);
                    if (self.peerDisconnectedEvents.has(peerId)) self.peerDisconnectedEvents.get(peerId)("default");
                }
            });
        }

        async emitMessage(opcode, payload, recipient, is_lan, lan_socket) {
            const self = this;
            return navigator.locks.request(recipient + "_tx", () => {
                let packet = { opcode };
                if (payload) packet.payload = payload;
                if (recipient) packet.recipient = recipient;
                if (is_lan) {
                    lan_socket.write(JSON.stringify(packet) + "\n");
                } else {
                    self.socket.send(JSON.stringify(packet));
                }
            });
        }

        async handleLANMessages(raw_packets, socket, is_host) {

            // Convert raw packet buffer to string and split by delimiter ('\n')
            const packets = raw_packets.toString().split("\n");
        
            // Process each complete packet
            const promises = packets.map((message) => {

                // Do nothing if message is empty
                if (message === "") return Promise.resolve();

                // Try to parse JSON
                try {
                    return this.handleSignalingMessage(JSON.parse(message), true, socket, is_host);
                } catch (err) {
                    console.error("Failed to parse JSON:", err, "Message:", message);
                    return Promise.resolve();
                }
            });
        
            // Wait for all promises to resolve
            return Promise.all(promises);
        }

        async handleSignalingMessage(message, is_lan = false, lan_socket = null, is_host = false) {
            const self = this;
            const { opcode, payload, recipient } = message;
            let origin;
            if (is_lan) {
                origin = lan_socket.metadata 
            }
            else {
                origin = message.origin
            };

            switch (opcode) {

                // Session ready
                case "INIT_OK":
                    self.id = payload.id;
                    self.session = payload.session_id;
                    self.usernameSet = true;
                    if (self.usernameSetEvent) self.usernameSetEvent(payload);
                    break;

                // Keep alive
                case "KEEPALIVE":
                    self.keepalive = setTimeout(() => {
                        self.emitMessage("KEEPALIVE", null, null, is_lan, lan_socket);
                    }, 5000) // 5 seconds delay
                    break;

                // Return list of open rooms
                case "LOBBY_LIST":
                    if (self.fetchedLobbyListEvent) self.fetchedLobbyListEvent(payload);
                    break;

                // Return lobby details
                case "LOBBY_INFO":
                    if (self.fetchedLobbyInfoEvent) self.fetchedLobbyInfoEvent(payload);
                    break;

                // Set mode
                case "ACK_HOST":
                    self.mode = 1;
                    break;
                case "ACK_PEER":
                    self.mode = 2;
                    break;

                // Do nothing
                case "ACK_META":
                case "RELAY_OK":
                case "PASSWORD_ACK":
                case "NEW_HOST":
                case "WARNING":
                case "LOBBY_NOTFOUND":
                case "LOBBY_FULL":
                case "LOBBY_LOCKED":
                case "PASSWORD_REQUIRED":
                case "PASSWORD_FAIL":
                case "LOBBY_EXISTS":
                case "ALREADY_HOST":
                case "ALREADY_PEER":
                    break;

                // Room ownership has changed
                case "HOST_RECLAIM":
                    if (payload.id === self.id) {
                        if (self.peerMadeOwnerEvent) self.peerMadeOwnerEvent();
                    }
                    break;
                
                // Close connections
                case "PEER_GONE":
                    await self.closeConnection(payload.id);
                    break;
                case "HOST_GONE":
                    await self.closeConnection(payload.id);
                    break;
                case "LOBBY_CLOSE":
                    self.Close();
                    break;
                case "TRANSITION":
                    console.log(`Changing to ${payload} mode...`);
                    self.mode = (payload == "host" ? 1 : 2);
                    self.peers.forEach(async (_, peerId) => {
                        await self.closeConnection(peerId);
                    });
                    self.emitMessage("TRANSITION_ACK", null, null);
                    if (self.modeChangeEvent) self.modeChangeEvent(payload);
                    break;
                
                // Prepare connections
                case "NEW_PEER":
                    return navigator.locks.request(payload.id, async () => {
                        await self.createConnection(payload.id, payload.user, payload.pubkey, is_lan, lan_socket)
                        await self.createOffer(payload.id, is_lan, lan_socket);
                        self.FireSeenPeerEvent(payload, opcode);
                    })

                case "ANTICIPATE":
                    return navigator.locks.request(payload.id, async () => {
                        await self.handleAnticipate(payload, is_lan, lan_socket);
                        self.FireSeenPeerEvent(payload, opcode);
                    })

                case "DISCOVER":
                    return navigator.locks.request(payload.id, async () => {
                        await self.handleDiscover(payload, is_lan, lan_socket);
                        self.FireSeenPeerEvent(payload, opcode);
                    })
                
                // Process offer
                case "MAKE_OFFER":
                    return navigator.locks.request(origin.id, async () => {
                        await self.handleOffer(origin.id, payload.contents, is_lan, lan_socket);
                        self.FireSeenPeerEvent(origin, opcode);
                    })
                
                // Process answer
                case "MAKE_ANSWER":
                    return navigator.locks.request(origin.id, async () => {
                        await self.handleAnswer(origin.id, payload.contents);
                    })
                
                // Process ICE
                case "ICE":
                    return navigator.locks.request(origin.id, async () => {
                        await self.handleICECandidate(origin.id, payload.contents);
                    })
                
                // Errors
                case "VIOLATION":
                    console.error("Protocol Violation:", payload);
                    self.Close();
                    break;
                default:
                    console.warn(`Unknown signaling message type: ${opcode}`);
            }

            if (self.eventCallbacks.has(opcode)) self.eventCallbacks.get(opcode)(payload, origin);
        }

        async MakeRoom(name, password, limit, relay, config) {
            const self = this;
            if (!self.username) return;

            if (self.in_lan_mode) {
                console.warn("Cannot create rooms in LAN mode");
                return;
            }

            let payload = {
                lobby_id: name,
                allow_host_reclaim: (config != "1"),
                allow_peers_to_claim_host: (config == "3"),
                max_peers: limit,
                password: password,
                use_server_relay: relay,
            };
            if (self.publicKey) payload.pubkey = self.publicKey;
            self.emitMessage("CONFIG_HOST", payload, null);
        }

        async JoinRoom(name, password) {
            const self = this;
            if (!self.username) return;

            if (self.in_lan_mode) {
                console.warn("Cannot join rooms in LAN mode");
                return;
            }

            let payload = {
                lobby_id: name,
                password: password,
            };
            if (self.publicKey) payload.pubkey = self.publicKey;
            self.emitMessage("CONFIG_PEER", payload, null);
        }

        async handleAnticipate(payload, is_lan = false, lan_socket = null) {
            const self = this;
            const { user, id, pubkey } = payload;
            if (lan_socket) {
                lan_socket.metadata.id = id;
                lan_socket.metadata.user = user;
            }
            await self.createConnection(id, user, pubkey, is_lan, lan_socket);
        }

        async handleDiscover(payload, is_lan = false, lan_socket = null) {
            const self = this;
            const { user, id, pubkey } = payload;
            if (lan_socket) {
                lan_socket.metadata.id = id;
                lan_socket.metadata.user = user;
            }
            await self.createConnection(id, user, pubkey, is_lan, lan_socket);
            await self.createOffer(id, is_lan, lan_socket);
        }

        Close() {
            const self = this;
            clearTimeout(self.keepalive);
            self.peers.forEach(async (_, peerId) => {
                await self.closeConnection(peerId);
            });
            self.relayEnabled = false;
            self.usernameSet = false;
            self.username = null;
            self.id = null;
            self.mode = 0;
            if (self.socket) {
                if (self.socket.readyState === WebSocket.OPEN) self.socket.close();
                self.socket = null;
            }
            self.broadcastStore = new Map(); 
        }
    }

    class CloudLinkPhi {
        constructor(vm) {
            this.vm = vm;

            // getting lobbies and info
            this.lobbylist = [];
            this.lobbyinfo = {};

            // handling peers connecting and disconnecting
            this.newestclient = {id: "", username: ""};
            this.lastclient = {id: "", username: ""};

            // Initialize client
            this.client = new PhiClient();

            this.client.WhenConnected(() => {
                this.vm.runtime.startHats("cloudlinkphi_on_signalling_connect");
            })

            this.client.WhenUsernameSet((session) => {
                this.vm.runtime.startHats("cloudlinkphi_on_signalling_login");
            })

            this.client.WhenDisconnected(() => {
                this.vm.runtime.startHats("cloudlinkphi_on_signalling_disconnect");
            })

            this.client.BindEvent("ACK_HOST", (data) => {
                console.log(`已创建房间。`);
            })

            this.client.WhenChangingMode((mode) => {
                console.log(`正在切换到 ${mode} 模式....`);
            })

            this.client.BindEvent("ACK_PEER", (data) => {
                console.log(`已加入房间。`);
            })

            this.client.OnOwnershipChange(() => {
                console.log("房间所有权已更改。此客户端现在是所有者。");
            })

            this.client.WhenPeerSupportsEncryption((origin) => {
                console.log(`${origin.user} (${origin.id}) 支持端到端加密。`);
            })

            this.client.OnGlobalBroadcast((channel, data, origin) => {
                this.vm.runtime.startHats('cloudlinkphi_on_broadcast_message');
            })

            this.client.OnNewPeer((origin, opcode) => {
                console.log(`发现新对等方 ${origin.user} (${origin.id}) 使用 ${opcode}。`);

                this.client.OnPeerPrivateMessage(origin.id, (channel, data, relayed) => {
                    console.log(`收到来自 ${origin.user} (${origin.id}) 在频道 ${channel} 的私信 (是否中继：${relayed})：${data}`);
                    this.vm.runtime.startHats('cloudlinkphi_on_private_message');
                })

                this.client.OnPeerConnected(origin.id, (channelid) => {
                    if (channelid == "default") {
                        console.log(`${origin.user} (${origin.id}) 已连接。`);
                        this.newestclient.id = origin.id;
                        this.newestclient.user = origin.user;
                        this.vm.runtime.startHats("cloudlinkphi_on_new_peer");
                        return;
                    }

                    console.log(`${origin.user} (${origin.id}) 已打开通道 ${channelid}。`);
                    this.vm.runtime.startHats("cloudlinkphi_on_dchan_open");
                })

                this.client.OnPeerDisconnected(origin.id, (channelid) => {
                    if (channelid == "default") {
                        console.log(`${origin.user} (${origin.id}) 已断开连接。`);
                        this.lastclient.id = origin.id;
                        this.lastclient.user = origin.user;
                        this.vm.runtime.startHats("cloudlinkphi_on_close_peer");
                        return;
                    }

                    console.log(`${origin.user} (${origin.id}) 已关闭通道 ${channelid}。`);
                    this.vm.runtime.startHats("cloudlinkphi_on_dchan_close");
                })

                this.client.OnPeerMessage(origin.id, (channel, data, relayed) => {
                    console.log(`收到来自 ${origin.user} (${origin.id}) 在频道 ${channel} 的消息 (是否中继：${relayed})：${data}`);
                    this.vm.runtime.startHats('cloudlinkphi_on_broadcast_message');
                })
            })
        }

        getInfo() {
            return {
                id: "cloudlinkphi",
                name: "CloudLink Φ",
                menuIconURI: "",
                blockIconURI: "",
                color1: '#FF9400',
                color2: '#FFAD73',
                color3: '#A16132',
                blocks: [
                    {
                        opcode: "extension_version",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "客户端版本",
                    },
                    {
                        opcode: "api_version",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "信令版本",
                    },
                    {
                        opcode: "protocol_version",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "协议版本",
                    },
                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '🔧 配置'
                    },
                    {
                        opcode: "isKeepaliveOn",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "是否启用保活？"
                    },
                    {
                        opcode: "isTurnOnlyModeOn",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "是否仅使用 TURN？"
                    },
                    {
                        opcode: "changeKeepalive",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "保活连接？ [KEEPALIVE]",
                        arguments: {
                            KEEPALIVE: {
                                type: Scratch2.ArgumentType.BOOLEAN,
                                defaultValue: false,
                            },
                        },
                    },
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: `除非你知道自己在做什么，`,
                    },
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '请不要更改下面的值。',
                    },
                    {
                        opcode: "changeTurnOnlyMode",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "只使用 TURN？ [TURNONLY]",
                        arguments: {
                            TURNONLY: {
                                type: Scratch2.ArgumentType.BOOLEAN,
                                defaultValue: false,
                            },
                        },
                    },
                    {
                        opcode: "change_stun_url",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "将 [URL] 用作 STUN",
                        arguments: {
                            URL: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "stun://vpn.mikedev101.cc:5349",
                            },
                        },
                    },
                    {
                        opcode: "change_turn_url",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "将 [URL] 用作 TURN，用户名 [USER] 密码 [PASS]",
                        arguments: {
                            URL: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "turn://vpn.mikedev101.cc:5349",
                            },
                            USER: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "free",
                            },
                            PASS: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "free",
                            },
                        },
                    },
                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '🔌 连接'
                    },
                    {
                        opcode: "on_signalling_connect",
                        blockType: Scratch2.BlockType.EVENT,
                        isEdgeActivated: false,
                        text: "当连接到服务器时",
                    },
                    {
                        opcode: "is_signalling_connected",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "已连接到服务器？",
                    },
                    {
                        opcode: "initialize",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "连接到服务器 [SERVER]",
						arguments: {
							SERVER: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "wss://phi.mikedev101.cc",
							},
						},
					},
                    {
                        opcode: "on_signalling_disconnect",
                        blockType: Scratch2.BlockType.EVENT,
                        isEdgeActivated: false,
                        text: "当断开连接时",
                    },
                    {
                        opcode: "leave",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "从服务器断开连接",
                    },
                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '🖥️ 局域网模式'
                    },
                    {
                        opcode: "lan_supported",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "是否支持局域网模式？"
                    },
                    "---",
                    {
                        opcode: "hosting_lan_server",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "我是否正在托管局域网服务器？",
                    },
                    {
                        opcode: "host_lan_server",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "以用户名 [USERNAME] 托管局域网服务器",
						arguments: {
                            USERNAME: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "apple",
                            },
						},
					},
                    {
                        opcode: "stop_lan_server",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "关闭局域网服务器",
                    },
                    "---",
                    {
                        opcode: "connected_to_lan",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "我是否已连接到局域网主机？",
                    },
                    {
                        opcode: "join_lan_server",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "以用户名 [USERNAME] 连接到 IP 为 [IP] 的局域网主机",
						arguments: {
							IP: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "127.0.0.1",
							},
                            USERNAME: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "banana",
                            },
						},
					},
                    {
                        opcode: "leave_lan_server",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "从局域网主机断开连接",
                    },
                    "---",
                    {
                        opcode: "looking_for_lan_servers",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "我是否正在扫描局域网主机？",
                    },
                    {
                        opcode: "find_lan_servers",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "查找局域网服务器",
                    },
                    {
                        opcode: "found_lan_servers",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "找到的所有局域网服务器",
                    },
                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '🙂 我的会话'
                    },
                    {
                        opcode: "on_signalling_login",
                        blockType: Scratch2.BlockType.EVENT,
                        isEdgeActivated: false,
                        text: "当用户名同步完成",
                    },
                    {
                        opcode: "my_ID",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "我的玩家 ID",
                    },
                    {
                        opcode: "my_Username",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "我的用户名",
                    },
                    {
                        opcode: "is_signaling_auth_success",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "用户名已同步？",
                    },
                    {
                        opcode: "authenticate",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "将用户名设置为 [TOKEN]",
						arguments: {
							TOKEN: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Apple",
							},
						},
					},
                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '👥 玩家'
                    },
                    {
                        opcode: "on_new_peer",
                        blockType: Scratch2.BlockType.EVENT,
                        shouldRestartExistingThreads: true,
                        isEdgeActivated: false,
                        text: "当玩家连接时",
                    },
                    {
                        opcode: "get_new_peer",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "最新连接的玩家",
                    },
                    "---",
                    {
                        opcode: "on_close_peer",
                        blockType: Scratch2.BlockType.EVENT,
                        shouldRestartExistingThreads: true,
                        isEdgeActivated: false,
                        text: "当玩家断开连接时",
                    },
                    {
                        opcode: "get_last_peer",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "最后断开连接的玩家",
                    },
                    {
                        opcode: "disconnect_peer",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "关闭与玩家 [PEER] 的连接",
						arguments: {
							PEER: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Player ID",
							},
						},
					},
                    "---",
                    {
                        opcode: "get_peers",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "所有已连接的玩家",
                    },
                    {
                        opcode: "is_peer_connected",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "是否已连接到玩家 [PEER]？",
						arguments: {
							PEER: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Player ID",
							},
						},
					},
                    {
                        opcode: "get_peer_username",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "获取 [PEER] 的用户名",
                        arguments: {
                            PEER: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Player ID",
                            },
                        },
                    },
                    {
                        opcode: "get_all_peer_matches",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "获取所有用户名匹配 [USERNAME] 的玩家 ID",
                        arguments: {
                            USERNAME: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Apple",
                            },
                        },
                    },
                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '🚪 房间'
                    },
                    {
                        opcode: "is_host",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "我是房主吗？",
                    },
                    {
                        opcode: "is_peer",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "我是房间成员吗？",
                    },
                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '将限制设置为零将会'
                    },
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '允许无限玩家加入。'
                    },
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '请务必谨慎操作。'
                    },
                    {
                        opcode: "init_host_mode",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "创建房间 [LOBBY]，将玩家数限制为 [PEERS]，密码为 [PASSWORD]，[CLAIMCONFIG]，使用服务器中继？ [USERELAY]",
						arguments: {
							LOBBY: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Apple",
							},
							PEERS: {
								type: Scratch2.ArgumentType.NUMBER,
								defaultValue: 0,
							},
							PASSWORD: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Banana",
							},
							CLAIMCONFIG: {
								type: Scratch2.ArgumentType.NUMBER,
								menu: "lobbyConfigMenu",
								defaultValue: 1,
							},
							USERELAY: {
								type: Scratch2.ArgumentType.BOOLEAN,
								defaultValue: false,
							}
						},
					},
                    "---",
                    {
                        opcode: "init_peer_mode",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "使用密码 [PASSWORD] 加入房间 [LOBBY]",
						arguments: {
							LOBBY: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Apple",
							},
							PASSWORD: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Banana",
							},
						},
					},
                    "---",
                    {
                        opcode: "lobby_list",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "所有房间",
                    },
                    {
                        opcode: "query_lobbies",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "刷新房间列表",
                    },
                    "---",
                    {
                        opcode: "lobby_info",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "房间信息",
                    },
                    {
                        opcode: "query_lobby",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "获取房间 [LOBBY] 的信息",
						arguments: {
							LOBBY: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Apple",
							},
						},
					},

                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '🔃 网络'
                    },
                    {
                        opcode: "on_broadcast_message",
                        blockType: Scratch2.BlockType.HAT,
                        isEdgeActivated: false,
                        shouldRestartExistingThreads: true,
                        text: "当在频道 [CHANNEL] 收到广播消息时",
						arguments: {
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "default",
							},
						},
					},
                    {
                        opcode: "get_global_channel_data",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "广播 [CHANNEL] 的数据",
						arguments: {
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "default",
							},
						},
					},
                    {
                        opcode: "get_global_channel_origin",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "广播 [CHANNEL] 的来源",
                        arguments: {
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "default",
							},
						},
					},
                    {
                        opcode: "broadcast",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "向所有拥有频道 [CHANNEL] 的人广播 [DATA] 并等待？ [WAIT] 使用服务器中继？ [RELAY]",
						arguments: {
							DATA: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Hello",
							},
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "default",
							},
							WAIT: {
								type: Scratch2.ArgumentType.BOOLEAN,
								defaultValue: false,
							},
                            RELAY: {
								type: Scratch2.ArgumentType.BOOLEAN,
								defaultValue: false,
							},
						},
					},
                    "---",
                    {
                        opcode: "on_private_message",
                        blockType: Scratch2.BlockType.HAT,
                        isEdgeActivated: false,
                        shouldRestartExistingThreads: true,
                        text: "当收到来自玩家 [PEER] 在频道 [CHANNEL] 的私信时",
						arguments: {
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "default",
							},
							PEER: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Player ID",
							},
						},
					},
                    {
                        opcode: "get_private_channel_data",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "来自玩家 [PEER] 的私有频道 [CHANNEL] 的数据",
						arguments: {
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "default",
							},
							PEER: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "ID",
							},
						},
					},
                    {
                        opcode: "send",
                        blockType: Scratch2.BlockType.COMMAND,
                        text: "通过频道 [CHANNEL] 向玩家 [PEER] 发送私有数据 [DATA] 并等待？ [WAIT]",
						arguments: {
							DATA: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Hello",
							},
							PEER: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Player ID",
							},
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "default",
							},
							WAIT: {
								type: Scratch2.ArgumentType.BOOLEAN,
								defaultValue: false,
							},
						},
					},
                    "---",
                    {
                        blockType: Scratch2.BlockType.LABEL,
                        text: '📡 通道'
                    },
                    {
                        opcode: "get_dchan_state",
                        blockType: Scratch2.BlockType.BOOLEAN,
                        text: "玩家 [PEER] 是否有频道 [CHANNEL]？",
                        arguments: {
                            PEER: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Player ID",
                            },
                            CHANNEL: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Apple",
                            },
                        }
                    },
                    {
                        opcode: "get_peer_channels",
                        blockType: Scratch2.BlockType.REPORTER,
                        text: "与玩家 [PEER] 的所有通道",
                        arguments: {
                            PEER: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Player ID",
                            },
                        }
                    },
                    "---",
                    {
						opcode: "on_dchan_open",
                        blockType: Scratch2.BlockType.HAT,
                        shouldRestartExistingThreads: true,
                        isEdgeActivated: false,
                        text: "当玩家 [PEER] 打开频道 [CHANNEL]",
                        arguments: {
                            PEER: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Player ID",
                            },
                            CHANNEL: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Apple",
                            },
                        }
					},
                    {
						opcode: "new_dchan",
						blockType: Scratch2.BlockType.COMMAND,
                        text: "与玩家 [PEER] 打开名为 [CHANNEL] 的新通道，并优先 [ORDERED]",
						arguments: {
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Apple",
							},
							PEER: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Player ID",
							},
							ORDERED: {
								type: Scratch2.ArgumentType.NUMBER,
								menu: "channelConfig",
								defaultValue: 1,
							},
						},
					},
                    "---",
                    {
						opcode: "on_dchan_close",
                        blockType: Scratch2.BlockType.HAT,
                        shouldRestartExistingThreads: true,
						isEdgeActivated: false,
                        text: "当玩家 [PEER] 关闭频道 [CHANNEL]",
                        arguments: {
                            PEER: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Player ID",
                            },
                            CHANNEL: {
                                type: Scratch2.ArgumentType.STRING,
                                defaultValue: "Apple",
                            },
                        }
					},
                    {
						opcode: "close_dchan",
						blockType: Scratch2.BlockType.COMMAND,
                        text: "关闭与玩家 [PEER] 名为 [CHANNEL] 的通道",
						arguments: {
							CHANNEL: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Apple",
							},
							PEER: {
								type: Scratch2.ArgumentType.STRING,
								defaultValue: "Player ID",
							},
						},
					},
                ],
                menus: {
					lobbyConfigMenu: {
						acceptReporters: true,
						items: [
                            {
                                text: "不要允许该房间被重新认领",
                                value: "1",
                            },
                            {
                                text: "允许服务器重新认领该房间",
                                value: "2",
                            },
                            {
                                text: "允许对等方重新认领该房间",
                                value: "3",
                            }
						],
					},
                    channelConfig: {
						acceptReporters: true,
						items: [
                            {
                                text: "可靠且有序",
                                value: "1",
                            },
                            {
                                text: "速度优先",
                                value: "2",
                            },
						],
					},
                },
            }
        }

        connected_to_lan() {
            const self = this;
            return Scratch2.Cast.toBoolean(self.client.is_lan_connected);
        }

        host_lan_server({USERNAME}) {
            const self = this;
            self.client.RunLANServer(Scratch2.Cast.toString(USERNAME));
        }

        hosting_lan_server() {
            const self = this;
            return Scratch2.Cast.toBoolean(self.client.hosting_lan);
        }

        stop_lan_server() {
            const self = this;
            self.client.StopLANServer();
        }

        join_lan_server({IP, USERNAME}) {
            const self = this;
            self.client.ConnectWithLAN(Scratch2.Cast.toString(IP), Scratch2.Cast.toString(USERNAME));
        }

        leave_lan_server() {
            const self = this;
            self.client.DisconnectFromLAN();
        }

        find_lan_servers() {
            const self = this;
            self.client.FindLANHosts();
        }

        looking_for_lan_servers() {
            const self = this;
            return Scratch2.Cast.toBoolean(self.client.is_lan_searching);
        }

        found_lan_servers() {
            const self = this;
            return Scratch2.Cast.toString(JSON.stringify(self.client.lan_hosts));
        }

        isKeepaliveOn() {
            const self = this;
            return Scratch2.Cast.toBoolean(self.client.enableKeepalive);
        }

        isTurnOnlyModeOn() {
            const self = this;
            return Scratch2.Cast.toBoolean(self.client.turn_only);
        }

        changeKeepalive({ KEEPALIVE }) {
            const self = this;
            self.client.enableKeepalive = Scratch2.Cast.toBoolean(KEEPALIVE);
        }

        changeTurnOnlyMode({ TURNONLY }) {
            const self = this;
            self.client.turn_only = Scratch2.Cast.toBoolean(TURNONLY);
        }

        change_stun_url({ URL }) {
            const self = this;
            self.client.stun_url = Scratch2.Cast.toString(URL);
        }

        change_turn_url({ URL, USER, PASS }) {
            const self = this;
            self.client.turn_url = Scratch2.Cast.toString(URL);
            self.client.turn_username = Scratch2.Cast.toString(USER);
            self.client.turn_password = Scratch2.Cast.toString(PASS);
        }

        extension_version() {
            const self = this;
			return Scratch2.Cast.toString(self.client.metadata.client_version);
		}

		api_version() {
            const self = this;
			return Scratch2.Cast.toString(self.client.metadata.signaling_version);
		}

		protocol_version() {
            const self = this;
			return Scratch2.Cast.toString(self.client.metadata.protocol_version);
		}
        
        lan_supported() {
            const self = this;
            return Scratch2.Cast.toBoolean(self.client.lan_feature_supported);
        }

        is_signalling_connected() {
            const self = this;
            return Scratch2.Cast.toBoolean(self.client.Connected());
        }

        is_signaling_auth_success() {
            const self = this;
            return Scratch2.Cast.toBoolean(self.client.usernameSet);
        }

        initialize({ SERVER }) {
            const self = this;
            self.client.Connect(Scratch2.Cast.toString(SERVER));
        }

        authenticate({ TOKEN }) {
            const self = this;
            self.client.SetUsername(Scratch2.Cast.toString(TOKEN));
        }

        async init_host_mode({ LOBBY, PEERS, PASSWORD, CLAIMCONFIG, USERELAY }) {
            const self = this;
            await self.client.MakeRoom(
                Scratch2.Cast.toString(LOBBY), 
                Scratch2.Cast.toString(PASSWORD), 
                Scratch2.Cast.toNumber(PEERS), 
                Scratch2.Cast.toBoolean(USERELAY),
                Scratch2.Cast.toNumber(CLAIMCONFIG));
        }

        async init_peer_mode({ LOBBY, PASSWORD }) {
            const self = this;
            await self.client.JoinRoom(Scratch2.Cast.toString(LOBBY), Scratch2.Cast.toString(PASSWORD));
        }

        lobby_list() {
            const self = this;
            return JSON.stringify(self.lobbylist);  
        }

        lobby_info() {
            const self = this;
            return JSON.stringify(self.lobbyinfo);
        }

        async query_lobby({ LOBBY }) {
            const self = this;
            return new Promise((resolve, reject) => {

                if (!self.client.Connected()) {
                    reject("Not connected to server.");
                    return;
                }
                self.client.emitMessage("LOBBY_INFO", Scratch2.Cast.toString(LOBBY), null);
                self.client.OnLobbyInfo((details) => {
                    this.lobbyinfo = details;
                    resolve();
                })
                this.client.BindEvent("CONFIG_REQUIRED", () => {
                    console.log("需要配置。（提示：请先设置你的用户名。）");
                    reject();
                })
                this.client.BindEvent("LOBBY_NOTFOUND", () => {
                    console.log("未找到房间。");
                    reject();
                })
            })
        }

        async query_lobbies() {
            const self = this;
            return new Promise((resolve, reject) => {
                if (!self.client.Connected()) {
                    reject("Not connected to server.");
                    return;
                }
                self.client.FetchRoomList();
                self.client.OnLobbyList((rooms) => {
                    this.lobbylist = rooms;
                    resolve();
                })
                this.client.BindEvent("CONFIG_REQUIRED", () => {
                    console.log("需要配置。（提示：请先设置你的用户名。）");
                    reject();
                })
            })
        }

        leave() {
            const self = this;
            self.client.Close();
        }

        async broadcast({DATA, CHANNEL, WAIT, RELAY}) {
            const self = this;

            if (Scratch2.Cast.toBoolean(RELAY)) {
                if (self.client.relayEnabled) {
                    // Use server-side relay to broadcast
                    return self.client.sendMessage(
                        "relay",
                        Scratch2.Cast.toString(CHANNEL),
                        "G_MSG",
                        Scratch2.Cast.toString(DATA),
                        true,
                        Scratch2.Cast.toBoolean(WAIT),
                    );
                }
            }

            // Manually broadcast
            let promises = new Array();
            for (let peer of self.client.peers.keys()) {
                promises.push(self.client.sendMessage(
                    peer,
                    Scratch2.Cast.toString(CHANNEL),
                    "G_MSG",
                    Scratch2.Cast.toString(DATA),
                    false,
                    Scratch2.Cast.toBoolean(WAIT),
                ));
            }
            return Promise.all(promises);
        }

        async send({DATA, PEER, CHANNEL, WAIT}) {
            const self = this;
            return self.client.sendMessage(
                Scratch2.Cast.toString(PEER),
                Scratch2.Cast.toString(CHANNEL),
                "P_MSG",
                Scratch2.Cast.toString(DATA),
                false,
                Scratch2.Cast.toBoolean(WAIT),
            );
        }

        on_dchan_open({ PEER, CHANNEL }) {
            const self = this;           
            if (!self.client.peers.has(Scratch2.Cast.toString(PEER))) return false;
            const peerConnection = self.client.peers.get(Scratch2.Cast.toString(PEER));
            if (!peerConnection.dataChannels.has(Scratch2.Cast.toString(CHANNEL))) return false;
            return true;
        }

        on_dchan_close({ PEER, CHANNEL }) {
            const self = this;
            if (!self.client.peers.has(Scratch2.Cast.toString(PEER))) return false;
            const peerConnection = self.client.peers.get(Scratch2.Cast.toString(PEER));
            if (!peerConnection.destroyedChannels.includes(Scratch2.Cast.toString(CHANNEL))) return false;
            return true;
        }

        async new_dchan({ CHANNEL, PEER, ORDERED }) {
            const self = this;
            return self.client.OpenChannel(
                Scratch2.Cast.toString(PEER),
                Scratch2.Cast.toString(CHANNEL),
                Scratch2.Cast.toBoolean(ORDERED)
            );
        }

        async close_dchan({ CHANNEL, PEER }) {
            const self = this;
            return self.client.CloseChannel(
                Scratch2.Cast.toString(PEER),
                Scratch2.Cast.toString(CHANNEL)
            );
        }

        get_new_peer() {
            const self = this;
            return JSON.stringify(self.newestclient);
        }

        get_last_peer() {
            const self = this;
            return JSON.stringify(self.lastclient);
        }

        get_peers() {
            const self = this;
            return JSON.stringify(Array.from(self.client.peers, ([id, peer]) => ({ id, username: peer.username })));
        }

        get_all_peer_matches({ USERNAME }) {
            const self = this;
            return JSON.stringify(Array.from(self.client.peers).filter(([_, peer]) => peer.username === USERNAME).map(([key]) => key));
        }

        get_peer_username({ PEER }) {
            const self = this;
            return (self.client.peers.has(Scratch2.Cast.toString(PEER))) ? Scratch2.Cast.toString(self.client.peers.get(Scratch2.Cast.toString(PEER)).username) : "";
        }

        get_peer_channels({ PEER }) {
            const self = this;
            if (!self.client.peers.has(Scratch2.Cast.toString(PEER))) return "[]";
            const peerConnection = self.client.peers.get(Scratch2.Cast.toString(PEER));
            return JSON.stringify(Array.from(peerConnection.dataChannels).map(([key]) => key));
        }

        is_peer_connected({ PEER }) {
            const self = this;
            return self.client.peers.has(Scratch2.Cast.toString(PEER));
        }

        get_dchan_state({ PEER, CHANNEL }) {
            const self = this;
            if (!self.client.peers.has(Scratch2.Cast.toString(PEER))) return false;
            if (!self.client.peers.get(Scratch2.Cast.toString(PEER)).dataChannels.has(Scratch2.Cast.toString(CHANNEL))) return false;
            return self.client.peers.get(Scratch2.Cast.toString(PEER)).dataChannels.get(Scratch2.Cast.toString(CHANNEL)).readyState === "open";
        }

        async disconnect_peer({ PEER }) {
            const self = this;
            await self.client.closeConnection(Scratch2.Cast.toString(PEER));
        }

        my_ID() {
            const self = this;
            return (self.client.id != null) ? Scratch2.Cast.toString(self.client.id) : "";
        }

        my_Username() {
            const self = this;
            return (self.client.username != null) ? Scratch2.Cast.toString(self.client.username) : "";
        }

        is_host() {
            const self = this;
            return self.client.mode == 1;
        }
        
        is_peer() {
            const self = this;
            return self.client.mode == 2;
        }

        on_broadcast_message({ CHANNEL }) {
            const self = this;
            if (!self.client.broadcastStore.has(Scratch2.Cast.toString(CHANNEL))) return false;
            return true;
        }

        get_global_channel_data({ CHANNEL }) {
            const self = this;
            if (!self.client.broadcastStore.has(Scratch2.Cast.toString(CHANNEL))) return "";
            const storage = self.client.broadcastStore.get(Scratch2.Cast.toString(CHANNEL));
            return Scratch2.Cast.toString(storage.data);
        }

        get_global_channel_origin({ CHANNEL }) {
            const self = this;
            if (!self.client.broadcastStore.has(Scratch2.Cast.toString(CHANNEL))) return "";
            const storage = self.client.broadcastStore.get(Scratch2.Cast.toString(CHANNEL));
            return Scratch2.Cast.toString(storage.origin);
        }

        on_private_message({ PEER, CHANNEL }) {
            const self = this;
            if (!self.client.peers.has(Scratch2.Cast.toString(PEER))) return false;
            const peerConnection = self.client.peers.get(Scratch2.Cast.toString(PEER));
            if (!peerConnection.privateStore.has(Scratch2.Cast.toString(CHANNEL))) return false;
            return true;
        }

        get_private_channel_data({ PEER, CHANNEL }) {
            const self = this;            
            if (!self.client.peers.has(Scratch2.Cast.toString(PEER))) return "";
            const peerConnection = self.client.peers.get(Scratch2.Cast.toString(PEER));
            if (!peerConnection.privateStore.has(Scratch2.Cast.toString(CHANNEL))) return "";
            return Scratch2.Cast.toString(peerConnection.privateStore.get(Scratch2.Cast.toString(CHANNEL)));
        }
    }

    // Initialize the extension and generate a key pair
    const extension = new CloudLinkPhi(Scratch2.vm);
    [extension.client.publicKey, extension.client.privateKey] = await extension.client.encryption.generateKeyPair();

    console.log("扩展已初始化：", extension);

    // Register the extension
    Scratch2.extensions.register(extension);
})(Scratch);
